Vue源码解析04--core.util-部分core模块通用函数

Core/Util主体结构

  • next-tickVue.nextTick的实现
  • props:用于合法化prop,并加入observe
  • options:Component构造函数中输入的options所需的相关处理
  • lang:一些通用的检测与类型转换的封装
  • error:针对ViewModel的错误信息的输出
  • perm:Performance API的封装
  • env:运行环境的检测
  • debug:警告信息与错误栈的生成与输出,方便调试时排错

1. util/next-tick.js

Vue.nextTick()的实现,其优先级为:

  1. Promise:micro
  2. setImmediate:macro
  3. MessageChannel:macro
  4. setTimeout:macro

建模

对于nextTick()方法,它需要以下变量来建模:

  • callbacks:回调队列,记录下一个tick要执行的所有回调
  • pending:指示是否正在运行上一个tick的回调
  • microTimerFunc:microtask的实现方式
  • macroTimerFunc:macrotask的实现方式
  • useMacroTask:指示是否使用macrotask
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 回调队列,记录下一个tick的所有回调
const callbacks = []
// 用于指示是否正在运行上一个tick的callbacks
let pending = false

//依次执行所有的回调,复位pending,并清空callbacks队列
function flushCallBacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for(let i = 0; i < copies.length; i++) {
copies[i]()
}
}

let microTimerFunc
let macroTimerFunc
// 用于指示是否使用macroTimerFunc
let useMacroTask = false

macroTimerFunc

决定使用MacroTask来执行的方式,优先级为:setImmediate > MessageChannel > setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 决定macroTimerFunc的实现。
* 先用setImmediate(仅IE支持),没有就用MessageChannel,最后使用setTimeout
*/
// 先尝试setImmediate
if(typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
}
// 然后尝试MessageChannel
else if(typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
}
// 最后尝试setTimeout
else {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

microTimerFunc

决定使用MicroTask执行的方式,首选Promise,不支持则转macroTimerFunc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 决定microTimerFunc的实现。
* 先用Promise,没有就直接套用MacroTimerFunc
*/
// 首先尝试Promise
if(typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)

// ios下要通过触发一个新的MacroTask,使得MicroTask在此之前执行
if(isIOS) setTimeout(()=>{}, 0)
}
} else {
microTimerFunc = macroTimerFunc
}

核心:Vue.nextTick()实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Vue.nextTick的实现
*/
function nextTick(cb?: Function, ctx?: Object) {
let _resolve

// 将回调以闭包形式加入堆栈,没有回调则以Promise的resolve作为回调
callbacks.push(() => {
if(cb) {
try {
cb.call(ctx)
} catech(e) {
handlerError(e, ctx, 'nextTick')
}
} else if(_resolve) {
_resolve(ctx)
}
})

// 非pending情况下,开始运行
if(!pending) {
pending = true
// 检查使用哪种task来触发
if(useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}

// 回调为空,则尝试返回一个Promise来进行回调操作
if(!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

其他的helper函数

1
2
3
4
5
6
7
8
9
// 返回一个闭包,使得该fn强制在MacroTask中执行
function withMacroTask(fn: Function) {
return fn._withTask || (fn._withTask = function() {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}

2. util/lang.js

提供一些通用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 检查某个字符串是否以$或_开头
function isReserved(str: String): boolean {
const c = (str + '').charCodeAt(0)
// 0x24为$,0x5F为_
return c === 0x24 || c ===0x5F
}

// Object.defineProperty的封装,其中enumerable默认为false
function def(obj: Object, key: string, val: any, enumerable: boolean = false) {
Object.defineProperty(obj, key, {
value: val,
enumerable,
writable: true,
configurable: true
})
}

// 转换路径,即在一个Object中逐层向内寻找,在watch中有使用
const bailRE = /[^\w.$]/
export function parsePath(path: string): any {
if(bailRE.test(path)) return

const segments = path.split('.')
return function(obj) {
for(let i = 0; i < segments.length; i++) {
if(!obj) return
obj = obj[segments[i]]
}
return obj
}
}

3. util/error.js

针对ViewModel的错误处理器,会输出错误栈以便进行追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 控制台输出错误
function logError(err, vm, info) {
if(process.env.NODE_ENV !== 'production') {
warn(`Error in ${info}: "${err.toString()}"`, vm)
}

if((inBrowser || inWeex) && typeof console !== 'undefined') {
console.log(err)
} else {
throw err
}
}

// 全局的ErrorHandler,从全局config中获取,并将其输出在控制台
function globalHandleError(err, vm, info) {
if(config.errorHandler) {
try {
return config.errorHandler.call(null, err, vm, info)
} catch(e) {
logError(e, null, 'config.errorHandler')
}
}
logError(err, vm, info)
}

// 处理错误,如果是vm中的错误,一层一层向上报出错误栈
function handleError(err: Error, vm: any, info: string) {
if(vm) {
let cur = vm
// 组件从自身开始一层层向上报出错误
while(cur = cur.$parent) {
const hooks = cur.$options.errorCaptured
if(hooks) {
for(let i = 0; i < hooks.length; i++) {
try {
const capture = hooks[i].call(cur, err, vm, info) === false
if(capture) return
} catch(e) {
globalHandleError(e, cur, 'errorCaptured hook')
}
}
}
}
}
globalHandleError(err, vm, info)
}

4. util/perm.js

Performance API相关的操作,用来记录操作发生的时间戳和计算某些操作所用的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export let mark
export let measure

if (process.env.NODE_ENV !== 'production') {
const perf = inBrowser && window.performance
// 检查浏览器是否支持Performance API
if (
perf &&
perf.mark &&
perf.measure &&
perf.clearMarks &&
perf.clearMeasures
) {
// 记录发生的时间戳
mark = tag => perf.mark(tag)
// 计算两次记录的时间间隔,完成后消除相关记录和计算记录
measure = (name, startTag, endTag) => {
perf.measure(name, startTag, endTag)
perf.clearMarks(startTag)
perf.clearMarks(endTag)
perf.clearMeasures(name)
}
}
}

5. util/env.js

env用于检测相关的系统环境。对于环境和设备类型的检测非常重要,可以根据其差异,来优化用户体验。

它包括了对以下环境变量和运行特性的检测:

  • __proto__
  • 运行环境:浏览器及其类型,Weex
  • 被动的eventListener,即不能preventDefault()
  • 是否为SSR
  • Vue Devtools插件
  • Symbol
  • Set
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 是否可以使用__proto__
export const hasProto = '__proto__' in {}

// 浏览器环境检测
export const inBrowser = typeof window !== 'undefined'
export const inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform
export const weexPlatform = inWeex && WXEnvironment.platform.toLowerCase()
export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
export const isIE = UA && /msie|trident/.test(UA)
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0
export const isEdge = UA && UA.indexOf('edge/') > 0
export const isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android')
export const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios')
export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
// Firefox有独特的Object.prototype.watch()方法
export const nativeWatch = ({}).watch

// 检查是否支持被动的eventListener,即preventDefault()没有效果
export let supportsPassive = false
if (inBrowser) {
try {
const opts = {}
Object.defineProperty(opts, 'passive', ({
get () {
supportsPassive = true
}
}: Object))
window.addEventListener('test-passive', null, opts)
} catch (e) {}
}

// 检查是否为Server Side Rendering,只能使用懒检查方式,因为SSR设置VUE_ENV晚于运行时
let _isServer
export const isServerRendering = () => {
if (_isServer === undefined) {
// 基本就是对Node.js环境的检测
if (!inBrowser && !inWeex && typeof global !== 'undefined') {
_isServer = global['process'].env.VUE_ENV === 'server'
} else {
_isServer = false
}
}
return _isServer
}

// 检测开发工具(Chrome的Vue Devtools插件)
export const devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__

// 检测某个class constructor是否原生支持
export function isNative (Ctor: any): boolean {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

// 是否支持ES6的Symbol
export const hasSymbol = typeof Symbol !== 'undefined' && isNative(Symbol) &&
typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys)

// Set类型的检测,顺带简单的polyfill
let _Set
if (typeof Set !== 'undefined' && isNative(Set)) {
_Set = Set
} else {
_Set = class Set implements SimpleSet {
set: Object;
constructor () {
this.set = Object.create(null)
}
has (key: string | number) {
return this.set[key] === true
}
add (key: string | number) {
this.set[key] = true
}
clear () {
this.set = Object.create(null)
}
}
}

6. util/debug.js

debug模式下的一些信息输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
export let warn
export let tip
export let generateComponentTrace
export let formatComponentName

if(process.env.NODE_ENV !== 'production') {
const hasConsole = typeof console !== 'undefined'

// 去除-和_,转为Pascal命名法
const classifyRE = /(?:^|[-_])(\w)/g
const classify = str => str
.replace(classifyRE, c => c.toUpperCase())
.replace(/[-_]/g, '')

// warn()方法,输出警告
warn = (msg, vm) => {
// vm存在时,输出组件错误栈
const trace = vm ? generateComponentTrace(vm) : ''
// 默认为console输出,除非特别指明了warnHandler
if (config.warnHandler) {
config.warnHandler.call(null, msg, vm, trace)
} else if (hasConsole && (!config.silent)) {
console.error(`[Vue warn]: ${msg}${trace}`)
}
}

// tip()方法,输出可行的优化方式
tip = (msg, vm) => {
if (hasConsole && (!config.silent)) {
console.warn(`[Vue tip]: ${msg}` + (vm ? generateComponentTrace(vm) : ''))
}
}

// 格式化组件的名称
// name有三个来源:options.name,options._componentTag,文件名(去除.vue后缀)
formatComponentName = (vm, includeFile) => {
if (vm.$root === vm) {
return '<Root>'
}
// 多渠道获取options
const options = typeof vm === 'function' && vm.cid != null
? vm.options
: vm._isVue
? vm.$options || vm.constructor.options
: vm || {}
let name = options.name || options._componentTag
const file = options.__file
if (!name && file) {
const match = file.match(/([^/\\]+)\.vue$/)
name = match && match[1]
}

return (
// 转为Pascal format
(name ? `<${classify(name)}>` : `<Anonymous>`) +
(file && includeFile !== false ? ` at ${file}` : '')
)
}

// 将一个字符串重复n遍,通过按位判断的方式,时间复杂度降为O(logn)
const repeat = (str, n) => {
let res = ''
while (n) {
if (n % 2 === 1) res += str
if (n > 1) str += str
n >>= 1
}
return res
}

// 生成组件栈,方便输出时追踪
generateComponentTrace = vm => {
if (vm._isVue && vm.$parent) {
const tree = []
let currentRecursiveSequence = 0
// 向父级递归,将合法的vm压入队列
while (vm) {
if (tree.length > 0) {
const last = tree[tree.length - 1]
if (last.constructor === vm.constructor) {
currentRecursiveSequence++
vm = vm.$parent
continue
} else if (currentRecursiveSequence > 0) {
tree[tree.length - 1] = [last, currentRecursiveSequence]
currentRecursiveSequence = 0
}
}
tree.push(vm)
vm = vm.$parent
}
return '\n\nfound in\n\n' + tree
.map((vm, i) => `${i === 0 ? '---> ' : repeat(' ', 5 + i * 2)}
${Array.isArray(vm)
? `${formatComponentName(vm[0])}... (${vm[1]} recursive calls)`
: formatComponentName(vm)
}`,
)
.join('\n')
} else {
return `\n\n(found in ${formatComponentName(vm)})`
}
}

}

Vue源码解析03--core

1. 模块

  • util:通用的方法。
  • instance:Vue实例的定义与生成。
  • observer:观察者,对组件实例属性的响应式改造,从而实现响应式更新。
  • vdom:vnode的生成与更新机制。
  • global-api:一些全局使用的API
  • component:keep-alive

2. index.js

core的入口主要做这几件事:

  • 初始化全局API
  • 定义Vue.prototype的$isServer$ssrContext属性
  • 定义Vue的FunctionalRenderContext属性
  • Vue.version = '__VERSION__'
  • export default Vue

3. config.js

core/config.js用于定义Vue全局设置的类型和初始设置。

该初始设置绝大部分涉及到shared目录下的共享对象和函数。

在初始状态下绝大部分为空值或空函数,需要在运行时中,根据环境和具体设置来进行二次设定。

Vue源码解析02--shared

1. constants.js 全局常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const SSR_ARRT = 'data-server-rendered'

// Vue中资源的三种类型名称
const ASSET_TYPES = {
'component',
'directive',
'filter'
}

// 组件生命周期钩子的名称
const LIFECYCLE_HOOKS = {
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
}

2. util.js 通用的函数

2.1 类型判断的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 空对象,不能做任何修改
const emptyObject = Object.freeze({})

// 检查是否为undefined或null(也可以用双等号==)
function isUndef (v: any): boolean { return v === undefined || v === null }
// 与isUndef相反
function isDef(v: any): boolean { return v !== undefined && v !== null }
// 判断是否为true,对应的还有 isFalse()
function isTrue(v: any): boolean { return v === true }
// 判断是否为非空的基本类型(number, string, symbol, boolean)
function isPrimitive(v: any): boolean {
return (typeof v === 'string' ||
typeof v === 'number' ||
typeof v === 'symbol' ||
typeof v === 'boolean'
)
}
// 判断是否为对象,注意先去除null的错误判断
function isObject(obj: mixed): boolean { return obj !== null || typeof obj === 'object' }

// Object.prototype.toString的alias
const _toString = Object.prototype.toString
// 用Object.prototype.toString来调取类型,比typeof更准确,能判断null和内置对象
function toRawType(v: any): string { return _toString.call(v).slice(8, -1) }
// 是否为纯对象,类似的还有isRegExp(),
function isPlainObject(obj: any): boolean { return _toString.call(obj)==='[object Object]' }

// 判断是否为有效的数组索引(自然数)
function isValidArrayIndex(val: any): boolean {
// 保留纯数字部分
const n = parseFloat(String(val))
// Array的index要为自然数
return n >= 0 && Math.floor(n) === n && isFinite(val)
}

2.2 对象相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 将任何类型转换为string
function toString(val: any): string {
return val == null ?
'' : typeof val === 'object' ? JSON.stringify(val, null, 2): String(val)
}
// 将一个输入string转换为number;如果失败,返回原string
function toNumber(val: string) {
const n = parseFloat(val)
return isNaN(n) ? val : n
}

// 返回一个map的闭包,用于检查某个key是否为它的属性
function makeMap(str: string, expectesLowerCase?: boolean): (key: string) => true | void {
const map = Object.create(null)
const list: Array<string> = str.split(',')
for(let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectedLowerCase ? val => map[val.toLowerCase]: val => map[val]
}
// makeMap生成的{slot: true, component: true}来检查元素的tag是否为Vue的内置tag
const isBuiltInTag = makeMap('slot,component', true)
// 检查元素某个属性是否为Vue的保留属性(key, ref, slot, slot-scope, is)
const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')

// 从arr中移除item
function remove(arr: Array<any>, item: any): Array<any> | void {
if(arr.lenght) {
const index = arr.indexOf(item)
// 可以使用(~index)替代
if(index > -1) {
return arr.splice(index, 1)
}
}
}

// Object.prototype.hasOwnProperty的alias
const hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn(obj: Object | Array<any>, key: string): boolean {
return hasOwnProperty.call(obj, key)
}

2.3 字符串转换函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建某个纯函数的闭包,该闭包有一个结果缓存,对于运算量较大的函数,可以减少重复运算
function cached<F>(fn: F): F {
const cache = Object.create(null)
return function cachedFn(str: string): any {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
}

// 将烤肉串式命名转为驼峰式命名
const camelizeRE = /-(\w)/g
const camelize = cached((str: string): string => {
return str.replace(camelizeRE, (match, p1) => p1 ? p1.toUppercase() : '')
})
// 将驼峰式命名转为烤肉串式命名
const hyphenRE = /\B([A-Z])/g
const hyphenate = cached((str: string): string => {
return str.replace(hyphenRE, '-$1').toLowerCase()
})

// 字符串首字母大写
const capitalize = cached((str: string): stirng => {
return str.charAt(0).toUpperCase() + str.slice(1)
})

2.4 Function相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
* bind的polyfill措施
*/
// 适用于PhantomJS 1.x的兼容性bind
function polyfillBind(fn: Function, ctx: Object): Function {
function boundFn(a) {
const len = arguments.length
return len
? len > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a)
: fn.call(ctx)
}

boundFn._length = fn.length
return boundFn
}
// 原生的bind
function nativeBind(fn: Function, ctx: Object): Function {
return fn.bind(ctx)
}
const bind = Function.prototype.bind ? nativeBind : polyfillBind

// Array-like转Array
function toArray(list: any, start: number = 0): Array<any> {
let i = list.length - start
const ret: Array<any> = new Array(i)
while(i--) {
ret[i] = list[i + start]
}
return ret
}

// 将一个对象混入另一个对象(并不是严格的继承)
function extend(to: Ojbect, from: Object): Object {
for(const key in from) if(from.hasOwnProperty(key)) to[key] = from[key]
return to
}
// 将一个Array<Object>转为Object
function toObject(arr: Array<Object>): Object {
const res = {}
for(let i = 0; i < arr.length; i++) {
if(arr[i]) extend(res, arr[i])
}
return res
}

// 从某个compiler模块生成静态键
function genStaticKeys(modules: Array<ModuleOptions>): string {
return modules.reduce((keys: Array<string>, m: ModuleOptions) => {
return keys.concat(m.staticKeys || [])
}, []).join(',')
}

// 广义相等检查,特别对于Object,只检查它们是否形式相同
function looseEqual(a: any, b: any): boolean {
if (a === b) return true

const isObjectA = isObject(a)
const isObjectB = isObject(b)
// 引用类型
if(isObjectA && isObjectB) {
try {
const isArrayA = Array.isArray(a)
const isArrayB = Array.isArray(b)
// 都是Array,检查每个对应元素是否相同
if(isArrayA && isArrayB) {
return a.length === b.length && a.every((value, index) => {
return looseEqual(value, b[index])
})
}
// 都不是Array,检查每个键值对是否相同
else if(!isArrayA && !isArrayB) {
const keysA = Object.keys(a)
const keysB = Object.keys(b)
return keysA.length === keysB.length && keysA.every(key => {
return looseEuqal(a[key], b[key])
})
}
// 类型不为同一种引用类型
else{
return false
}
} catch(e) {
return false
}
}
// 非引用类型,转为string处理
else if(!isObjectA && !isObjectB) {
return String(a) === String(b)
}
// 两者类型不同
else {
return false
}
}

// 利用广义相等检查来进行索引查找
function looseIndexOf(arr: Array<mixed>, val: mixed): number {
for(let i = 0; i < arr.length; i++) {
if(looseEqual(arr[i], val)) return i
}
return -1
}

// 保证某个函数只运行一次
function once = (fn: Function): Function {
let called = false
return function() {
if(!called) {
called = true
fn.apply(this, arguments)
}
}
}